aboutsummaryrefslogtreecommitdiff
path: root/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx')
-rw-r--r--src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx140
1 files changed, 140 insertions, 0 deletions
diff --git a/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx b/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx
new file mode 100644
index 0000000..fdd8a14
--- /dev/null
+++ b/src/app/(main)/websites/[websiteId]/(reports)/retention/Retention.tsx
@@ -0,0 +1,140 @@
+import { Column, Grid, Icon, Row, Text } from '@umami/react-zen';
+import type { ReactNode } from 'react';
+import { LoadingPanel } from '@/components/common/LoadingPanel';
+import { Panel } from '@/components/common/Panel';
+import { useLocale, useMessages, useResultQuery } from '@/components/hooks';
+import { Users } from '@/components/icons';
+import { formatDate } from '@/lib/date';
+import { formatLongNumber } from '@/lib/format';
+
+const DAYS = [1, 2, 3, 4, 5, 6, 7, 14, 21, 28];
+
+export interface RetentionProps {
+ websiteId: string;
+ startDate: Date;
+ endDate: Date;
+ days?: number[];
+}
+
+export function Retention({ websiteId, days = DAYS, startDate, endDate }: RetentionProps) {
+ const { formatMessage, labels } = useMessages();
+ const { locale } = useLocale();
+ const { data, error, isLoading } = useResultQuery('retention', {
+ websiteId,
+ startDate,
+ endDate,
+ });
+
+ const rows =
+ data?.reduce((arr: any[], row: { date: any; visitors: any; day: any }) => {
+ const { date, visitors, day } = row;
+ if (day === 0) {
+ return arr.concat({
+ date,
+ visitors,
+ records: days
+ .reduce((arr, day) => {
+ arr[day] = data.find(
+ (x: { date: any; day: number }) => x.date === date && x.day === day,
+ );
+ return arr;
+ }, [])
+ .filter(n => n),
+ });
+ }
+ return arr;
+ }, []) || [];
+
+ const totalDays = rows.length;
+
+ return (
+ <LoadingPanel data={data} isLoading={isLoading} error={error}>
+ {data && (
+ <Panel allowFullscreen height="900px">
+ <Column
+ paddingY="6"
+ paddingX={{ xs: '3', md: '6' }}
+ position="absolute"
+ top="40px"
+ left="0"
+ right="0"
+ bottom="0"
+ >
+ <Column gap="1" overflow="auto">
+ <Grid
+ columns="120px repeat(10, 100px)"
+ alignItems="center"
+ gap="1"
+ height="50px"
+ width="max-content"
+ minWidth="100%"
+ autoFlow="column"
+ >
+ <Column>
+ <Text weight="bold" align="center">
+ {formatMessage(labels.cohort)}
+ </Text>
+ </Column>
+ {days.map(n => (
+ <Column key={n}>
+ <Text weight="bold" align="center" wrap="nowrap">
+ {formatMessage(labels.day)} {n}
+ </Text>
+ </Column>
+ ))}
+ </Grid>
+ {rows.map(({ date, visitors, records }: any, rowIndex: number) => {
+ return (
+ <Grid
+ key={rowIndex}
+ columns="120px repeat(10, 100px)"
+ gap="1"
+ autoFlow="column"
+ width="max-content"
+ minWidth="100%"
+ >
+ <Column justifyContent="center" gap="1">
+ <Text weight="bold">{formatDate(date, 'PP', locale)}</Text>
+ <Row alignItems="center" gap>
+ <Icon>
+ <Users />
+ </Icon>
+ <Text>{formatLongNumber(visitors)}</Text>
+ </Row>
+ </Column>
+ {days.map(day => {
+ if (totalDays - rowIndex < day) {
+ return null;
+ }
+ const percentage = records.filter(a => a.day === day)[0]?.percentage;
+ return (
+ <Cell key={day}>
+ {percentage ? `${Number(percentage).toFixed(2)}%` : ''}
+ </Cell>
+ );
+ })}
+ </Grid>
+ );
+ })}
+ </Column>
+ </Column>
+ </Panel>
+ )}
+ </LoadingPanel>
+ );
+}
+
+const Cell = ({ children }: { children: ReactNode }) => {
+ return (
+ <Column
+ justifyContent="center"
+ alignItems="center"
+ width="100px"
+ height="100px"
+ backgroundColor="2"
+ borderRadius
+ >
+ {children}
+ </Column>
+ );
+};